fix(sdk,desktop,cli): tag reactions with the target author (NIP-25 p tag)#2634
fix(sdk,desktop,cli): tag reactions with the target author (NIP-25 p tag)#2634shani-singh1 wants to merge 2 commits into
Conversation
…tag)
Every kind:7 Buzz emits carries only `["e", <target>]`. NIP-25 also requires
the reacted-to author's `p` tag, and that tag is the mechanism a notification
feed keys on: without it `{"kinds":[7],"#p":[<self>]}` can never match, so a
reaction to your message is invisible to every NIP-01 client — including
Amethyst, which is where this was reported from.
It is not only an interop gap. The relay already advertises kind 7 in
`PUSH_KINDS` (push_lease.rs) and the lease subscription shape is exactly
`{"kinds":[7],"#p":[...]}`, so the push surface is built for a tag no Buzz
client produces.
Thread the target author through every reaction producer:
- buzz-sdk `build_reaction` / `build_custom_emoji_reaction` take the author
and emit the `p` tag. Required rather than optional so a caller cannot
silently reintroduce the non-conformant form.
- Both builders set `.allow_self_tagging()`. nostr 0.44 strips a `p` tag
matching the signer, which would have quietly dropped the tag whenever
someone reacts to their own message; a regression test pins this.
- The desktop `add_reaction` command takes the target pubkey from the caller.
The timeline already resolved it (`resolveEventAuthorPubkey`), so this uses
the displayed author rather than the raw signer — for relay-signed agent
messages those differ and the displayed author is who should be notified.
- buzz-cli resolves the author with an `ids:[...]` lookup, since it is only
given an event id.
- buzz-acp's 👀/💬 status reactions carry the tag too; the author is already
on the batch event.
No in-app behaviour changes: neither the desktop feed nor the mobile activity
filter includes kind 7, and the ACP mention subscription is kinds
[9, approval, reminder], so p-tagged reactions do not wake agents.
Fixes block#2568
Signed-off-by: Shani Singh <teamdeveloperworld@gmail.com>
fa91956 to
1dd3c7e
Compare
|
P0 triage — merge candidate. 🐝 Confirmed the interop gap: kind:7 emits carried only the The plumbing looks right: Ask before merge: any other kind:7 emit sites outside |
The Flutter client had the same gap as the desktop/CLI/SDK paths: its
`addReaction` emitted only `["e", target]` plus an optional NIP-30 emoji tag,
so mobile-authored reactions were invisible to any `{"kinds":[7],"#p":[self]}`
notification filter.
`ChannelActions.addReaction` now takes the target author and emits its `p`
tag. All four call sites already hold it — channel and thread rows and the
message-action sheet pass `TimelineMessage.pubkey`, and Pulse threads
`UserNote.pubkey` through `toggleNoteUpvote`.
Unlike the Rust builders this needs no self-tagging opt-out: the Dart signer
passes tags through verbatim, so a self-reaction keeps its `p` tag.
Found while confirming there were no remaining kind:7 emit sites without the
tag. The other two `Kind::Custom(7)` constructions in the tree
(`buzz-db/src/event.rs`, `desktop/src-tauri/src/commands/social.rs`) are
`#[cfg(test)]` fixtures.
Signed-off-by: Shani Singh <teamdeveloperworld@gmail.com>
|
Thanks — and the negative wasn't clean, so good question to ask. Swept the tree for kind:7 construction across all four languages and found one more emit site the original push missed: the Flutter client.
One asymmetry worth noting: mobile needs no self-tagging opt-out. The Dart signer ( Full sweep, for the record:
Two deliberate exclusions:
Caveat on the mobile commit, stated plainly: there's no Dart toolchain on this machine (hermit's bootstrap fails on Windows), so I could not run |
Fixes #2568.
Problem
Every kind:7 Buzz emits carries only the target
etag:{"kind":7,"tags":[["e","8f8c92f8…"]],"content":"👍", …}NIP-25 also requires the reacted-to author's
ptag. That tag is not cosmetic — it is the only thing a notification feed can key on. Without it,{"kinds":[7],"#p":[<self>]}can never match, so a reaction to your message is invisible to every NIP-01 client, which is what @vitorpamplona hit from Amethyst.It isn't only an interop gap. The relay already ships kind 7 as push-notifiable —
PUSH_KINDS = &[7, 9, 1059, 40007, 46010](crates/buzz-relay/src/handlers/push_lease.rs:15) — and the lease subscription shape is literally{"kinds":[7],"#p":[…]}. The push surface is built around a tag no Buzz client produces.Four producers were affected:
buzz-sdk, the desktop Tauri command,buzz-cli, andbuzz-acp's 👀/💬 status reactions.Solution
Thread the target author through every reaction producer.
buzz-sdk—build_reaction/build_custom_emoji_reactiontaketarget_author: nostr::PublicKeyand emit["p", <author>]. Required rather thanOption, so a caller can't silently reintroduce the non-conformant form.Both builders now set
.allow_self_tagging(). This is the subtle part: reacting to your own message makes theptag match the signer, and nostr 0.44 strips matchingptags by default. Without it, self-reactions would quietly regress to the old wire form. The repo already relies on this for NIP-IA (build_archive_identity_request); the reaction path now does too, pinned by a regression test.Desktop —
add_reactiontakestarget_pubkeyfrom the caller. The timeline already resolved it viaresolveEventAuthorPubkey, so this uses the displayed author, not the raw signer. For relay-signed agent messages those differ, and the displayed author is who should be notified. Passing it in also avoids a second relay round trip on a hot UI path.buzz-cli— only ever gets an event id, so it resolves the author with a kindlessids:[…]lookup. That filter shape is explicitly exempt from the#pread gate (req.rs:1061, "a kindlessidslookup is unaffected"). A target that doesn't resolve here would have been rejected by ingest anyway (invalid: reaction target event not found); failing early just gives a clearer message.buzz-acp— the 👀/💬 indicators are reactions like any other, so they carry the tag too. The author is already on the batch event;react_workingnow takes(event_id, author)pairs. Removal is unchanged — a kind:5 delete keys off the reaction's own id, soReactionGuardstill only needs ids.Tag order is
e,p,emoji. Ingest resolves the reaction target from the lastetag (ingest.rs:2247), so adding aptag cannot shift target resolution.Why this is behaviour-neutral in-app
Worth stating explicitly, since "add a p tag" could plausibly mean "start waking agents on every 👍":
[9, 40002, 1, 45001, 45003](commands/messages.rs:72) — no kind 7.activity_provider.dart:33).[KIND_STREAM_MESSAGE, KIND_WORKFLOW_APPROVAL_REQUESTED, KIND_STREAM_REMINDER](config.rs:1157) — no kind 7, so p-tagged reactions do not wake agents.P_GATED_KINDS, so no read path changes.The only things that change are NIP-25 conformance on the wire and the relay's already-advertised push surface becoming reachable.
Validation
Toolchain note: this was built on Windows with the
x86_64-pc-windows-gnutoolchain (no MSVC linker locally), so clippy is 1.97 rather than the pinned 1.95.cargo test -p buzz-sdk --lib— 234 passed, 0 failed, including the newreaction_keeps_p_tag_when_reacting_to_own_message. I verified that test has teeth: with.allow_self_tagging()removed it fails withassertion failed: has_tag(&ev, "p", …), confirming the nostr 0.44 scrub is real and not theoretical.cargo check -p buzz-sdk -p buzz-cli -p buzz-acp— clean.cargo fmt -p buzz-sdk -p buzz-cli -p buzz-acp -- --check— clean.cargo clippy -p buzz-sdk -p buzz-cli -p buzz-acp --all-targets -- -D warnings— the only findings are two pre-existingquestion_marklints incrates/buzz-acp/src/queue.rs(313, 942), a file this PR does not touch; they come from the newer clippy, not from this change.cargo checkondesktop/src-tauri— clean (24 pre-existing dead-code warnings inmanaged_agents/, none from this PR).pnpm typecheckandpnpm lintindesktop/— both clean.biome formaton the six changed TS files — clean. (Checked against LF copies; my checkout iscore.autocrlf=true, which makes biome report all 1600 files unformatted locally.)ERR_UNSUPPORTED_ESM_URL_SCHEME … Received protocol 'c:'on a clean tree too. No test referencesadd_reaction, so nothing in that suite covers this path either way.The E2E mock (
e2eBridge.ts) now emits theptag as well, soinbox-reactions.spec.ts's "real add_reaction wire shape" assertion stays honest. It falls back to reading the author out of its own mock event store when a spec drivesadd_reactiondirectly without one, so the two existing specs that do that keep working unchanged.Follow-up, not done here
NIP-25 also suggests a
ktag carrying the target's kind, which helps clients filter reaction notifications. Every call site now has the target event in hand so it would be cheap, but it's a separate wire change and I kept this PR to the reported bug.